iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Claude AI

從現場踩坑到 AI 工具 — IT Diagnostic Agent 開發實錄系列 第 17

Day 17 — Claude Adapter 實作,以及 Gemini 看起來很像其實完全不一樣

  • 分享至 

  • xImage
  •  

系列:從現場踩坑到 AI 工具 — IT Diagnostic Agent 開發實錄


我原本的預期

Claude adapter 寫完之後,我要接 Gemini。

當時的心態很輕鬆:都是 LLM API,都是送一段對話過去、拿一段文字回來,應該複製貼上改個網址就好。

我錯了。而且錯得很徹底。

兩家在八個維度上完全不一樣,沒有一個地方可以直接沿用。


先看 Claude 那版

async function callClaude(messages, systemPrompt){
  const p = providers.claude;
  if(!p.key) throw new Error('Claude API Key not set');
  const res = await fetch('https://api.anthropic.com/v1/messages', {
    method:'POST',
    headers:{
      'Content-Type':'application/json',
      'x-api-key':p.key,
      'anthropic-version':'2023-06-01',
      'anthropic-dangerous-direct-browser-access':'true'
    },
    body:JSON.stringify({model:p.model, max_tokens:1024, system:systemPrompt, messages})
  });
  const data = await res.json();
  if(data.error) throw new Error(data.error.message);
  return data.content[0].text;
}

我覺得這段程式碼很乾淨,原因是 Anthropic 的 API 設計本身很直白:

  • Key 放 header
  • 模型名放 body
  • system prompt 是 body 裡一個獨立的頂層欄位
  • messages 直接原封不動送過去
  • 回覆從 data.content[0].text

特別是 system 那個欄位。 它把系統提示當成一個跟對話分開的一級概念,這在觀念上很清楚:systemPrompt 不是對話的一部分,它是整場對話的規則。

(考慮到我第三章整章都在講「systemPrompt 是對話憲法」,這個 API 設計選擇跟我的心智模型剛好吻合。)


再看 Gemini 那版

async function callGemini(messages, systemPrompt){
  const p = providers.gemini;
  if(!p.key) throw new Error('Gemini API Key not set');
  // Convert messages to Gemini format
  const contents = messages.map(m => ({
    role: m.role === 'assistant' ? 'model' : 'user',
    parts: [{text: m.content}]
  }));
  const res = await fetch(
    `https://generativelanguage.googleapis.com/v1beta/models/${p.model}:generateContent?key=${p.key}`, {
    method:'POST',
    headers:{'Content-Type':'application/json'},
    body:JSON.stringify({
      system_instruction:{parts:[{text:systemPrompt}]},
      contents,
      generationConfig:{maxOutputTokens:1024}
    })
  });
  const data = await res.json();
  if(data.error) throw new Error(data.error.message);
  return data.candidates[0].content.parts[0].text;
}

注意那行註解:// Convert messages to Gemini format

那行註解就是重點。 Claude 版沒有任何轉換,messages 直接送。Gemini 版必須先重建整個資料結構。


八個維度的差異

我把兩邊逐項列出來:

# 維度 Claude Gemini
1 Key 放哪 x-api-key header URL query string ?key=
2 模型名放哪 body 的 model 欄位 URL 路徑裡 models/{model}:generateContent
3 system prompt 形狀 頂層 system 字串 system_instruction:{parts:[{text}]}
4 助理的 role 叫什麼 assistant model
5 訊息內容形狀 content 字串 parts:[{text}] 陣列
6 訊息陣列叫什麼 messages contents
7 max tokens 放哪 頂層 max_tokens generationConfig.maxOutputTokens
8 回覆在哪 data.content[0].text data.candidates[0].content.parts[0].text

八項全中。沒有任何一項相同。


幾個值得單獨說的

Key 放在 URL 裡

這個我第一次看到有點意外。

?key=${p.key}

API Key 出現在 URL query string 裡。這代表它會被寫進瀏覽器歷史紀錄、被寫進任何中間節點的存取日誌、被 Referer header 帶走。

Google 有他們的理由(相容性、簡化 CORS 處理),而且這是他們官方文件的做法。但從一個資安的角度看,header 比 query string 安全得多,這是基本常識。

有趣的對比是:Claude 需要一個名字裡寫著 dangerous 的 header 才能從瀏覽器呼叫,而 Gemini 直接就通,代價是 Key 放在 URL 裡。

兩家對「瀏覽器直連」這件事的態度,透露了不同的安全哲學。

模型名在 URL 路徑裡

models/${p.model}:generateContent

這是 REST 風格的設計:模型是一個資源,generateContent 是對它的動作。

從 API 設計的角度是合理的。但從 adapter 實作的角度,它意味著換模型要重組 URL,而不是改一個欄位

如果你的程式碼有做 URL 快取或字串常數,這裡會咬你。

assistant vs model

role: m.role === 'assistant' ? 'model' : 'user'

這一行是整段轉換裡最容易出錯的地方。

我的對話歷史內部統一用 assistant(因為 Claude 和 OpenAI 都用這個),到 Gemini 這裡要翻成 model

如果漏掉這行轉換,API 不會清楚地告訴你「role 不合法」,它可能給你一個很模糊的錯誤,或是把整段對話當成使用者發言處理,然後回一個很怪的答案。

這種錯誤最難查,因為它不會 crash,只會讓品質變差。

回覆藏在四層裡面

data.candidates[0].content.parts[0].text

四層。而 Claude 是兩層 data.content[0].text

candidates 這個命名反映了 Gemini 的設計:一次請求可以回多個候選答案。這是能力更強的介面設計。

但對只要一個答案的應用來說,這是每次都要付的四層稅。而且每一層都是一個可能 undefined 的地方——如果 API 回了空的 candidates(例如觸發安全過濾),這行就直接炸了。


「相容」和「相似」是兩件不同的事

寫完這兩個 adapter,我對「多模型支援」的認知變了。

我原本以為 LLM API 已經走向標準化,各家大同小異。

實際上:它們在概念上相似,在介面上毫無共通性。

概念相似的意思是:都有系統提示、都有對話歷史、都有生成參數、都回文字。任何一個工程師看完 Claude API 都能猜到 Gemini API 大概有哪些功能。

介面毫無共通性的意思是:那些功能對應到的欄位名稱、資料形狀、巢狀層級、放置位置,全部不一樣。

而寫程式的人要處理的是後者。


這正是 Adapter Pattern 存在的理由

如果沒有 Day 16 那個統一契約,這八項差異會滲透到整個程式碼庫。

因為有了契約,這八項差異被壓縮在一個函式裡。

上層程式碼呼叫 callLLM(messages, systemPrompt),它不知道也不需要知道:

  • Key 在 header 還是 URL
  • 助理的 role 叫什麼
  • 回覆藏在第幾層

這是 Adapter Pattern 的價值最具體的一次體現:它把「八個維度都不一樣」這個混亂,關進了一個二十行的函式裡。


結構化地回顧

核心 vs 外部
核心是「取得一段診斷回覆」。欄位名稱、巢狀結構、Key 放哪裡,全部是外部細節。

Adapter 的職責就是在核心與外部之間畫一條線,並且獨自承受線外的所有混亂

已成立 vs 假設
「LLM API 大同小異」是我的假設。逐行比對之後它被推翻了。

代價是我多花了時間,收穫是我不再假設下一家會很像。後來接 OpenAI 相容 API 的時候,我是先讀完文件才動手的。


今天的反思

工程師最貴的錯誤,往往不是技術能力不足,是用第一個案例的經驗去推論第二個案例

我看完 Claude API 就以為自己懂了 LLM API。實際上我懂的是 Anthropic 對 LLM API 的一種設計選擇。

一個樣本推論不出模式。 這句話在排障上我很清楚——一台電腦壞了不代表是網路問題。但在寫程式時我照樣犯。


明天預告: Gemini 教會我不要假設相似。但接下來 OpenAI 相容 API 又給了我一個反向的教訓——這次真的可以複製貼上,而且一次支援好幾家。但「相容」不等於「行為一致」,我在 Ollama 的錯誤處理上留下了證據。


作者:Rich Chang | IT 基礎建設工程師 | 越南・柬埔寨・台灣


上一篇
Day 16 — 如果明天 Claude 漲價怎麼辦?
下一篇
Day 18 — OpenAI 相容 API:一次支援 Kimi、Groq、DeepSeek
系列文
從現場踩坑到 AI 工具 — IT Diagnostic Agent 開發實錄22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言